#include "ui/vsQtGui.h"

#include <QMainWindow>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QFormLayout>
#include <QListView>
#include <QHeaderView>
#include <QPlainTextEdit>
#include <QLabel>
#include <QSpinBox>
#include <QCheckBox>
#include <QLineEdit>
#include <QScrollBar>
#include <QScrollArea>
#include <QPushButton>
#include <QGroupBox>
#include <QListWidget>
#include <QTextEdit>
#include <QTextBrowser>
#include <QFileDialog>

#include <QDateTime>
#include <QSettings>
#include <QApplication>
#include <QAction>
#include <QMenu>
#include <QDialog>
#include <QClipboard>
#include <QWidgetAction>
#include <QDir>
#include <QProcess>
#include <QDesktopServices>

#include <QUrl>
#include <QMimeData>
#include <QDragEnterEvent>
#include <QDragMoveEvent>
#include <QDropEvent>
#include <QTimer>

#include "ui/vsMLModel.h"
#include "ui/vsMLDelegate.h"
#include "io/vsMedia.h"

#include <math.h>


const QString c_uri = "text/uri-list";

const QString c_vs_version              = QObject::tr("Vocaseed Beta");
const QString c_greet_msg               = QObject::tr("Launching ") + c_vs_version;
const QString c_vs_descr                = QObject::tr("VocaSeed :: media webseed server");
const QString c_default_headers         = "name artist path status id type size requests uprate seeded duration";
const QString c_default_header_widths   = "86 50 185 63 36 84 50 60 76 100 59";

// default app window geometry
const int c_window_width  = 825;
const int c_window_height = 438;

const int c_port_min = 20;
const int c_port_max = 64000;

const QString c_files_tab_name    = QObject::tr("Files");
const QString c_player_tab_name   = QObject::tr("Player");
const QString c_settings_tab_name = QObject::tr("Settings");
const QString c_log_tab_name      = QObject::tr("Log");

const int c_log_tab_index         = 2; // change if there'll be more tabs inbetween, or tab dragging/order change


//----------------------------------------------------------------------------------
dWgt::dWgt(QWidget *parent) : QWidget(parent)   {setAcceptDrops(true);}
void dWgt::dragEnterEvent(QDragEnterEvent*event){if(event->mimeData()->hasFormat(c_uri)) event->acceptProposedAction();}
void dWgt::dragMoveEvent (QDragMoveEvent *event){if(event->mimeData()->hasFormat(c_uri)) event->acceptProposedAction();}

void dWgt::dropEvent(QDropEvent *event) {
    foreach (const QByteArray &uriData, event->mimeData()->data("text/uri-list").split('\n'))
        if (!uriData.isEmpty())
            emit fileDropped(QUrl::fromEncoded(uriData).toLocalFile().remove('\r')); }
//----------------------------------------------------------------------------------


vsWindow::vsWindow(QWidget *parent, Qt::WindowFlags flags) : QWidget(parent, flags)
{
    QSettings s(c_def_appsettings_init);
    lastDir = s.value("last_directory").toString();

    // tray ===========================================================
    trayAvailable = QSystemTrayIcon::isSystemTrayAvailable();

    restoreAction = new QAction(tr("&Restore"), this);
    connect(restoreAction, SIGNAL(triggered()), this, SLOT(showNormal()));

    quitAction = new QAction(tr("&Quit"), this);
    connect(quitAction, SIGNAL(triggered()), this, SLOT(onExit()));

    trayIconMenu = new QMenu(this);
    trayIconMenu->addAction(restoreAction);
    trayIconMenu->addSeparator();
    trayIconMenu->addAction(quitAction);

    trayIcon = new QSystemTrayIcon(this);
    trayIcon->setContextMenu(trayIconMenu);
    trayIcon->setIcon(QIcon(":/icons/res/appicon.png"));
    trayIcon->setToolTip(c_vs_descr);

    connect(trayIcon, SIGNAL(activated(QSystemTrayIcon::ActivationReason)),
            this, SLOT(iconActivated(QSystemTrayIcon::ActivationReason)));

    // main page ======================================================
    filesPage = new dWgt();

    // crossbreed between list and table
    QScrollBar *vscr = new QScrollBar(Qt::Vertical);
    mlHeader         = new QHeaderView(Qt::Horizontal);
    mediaList        = new QListView();
    search           = new QLineEdit();
    searchBtn        = new QPushButton(QIcon(":/icons/res/search.png"), "");

    QGridLayout* gl  = new QGridLayout();
    gl->setSpacing(0);
    gl->setContentsMargins(QMargins(0,0,0,0));

    gl->addWidget(mlHeader,  0, 0);
    gl->addWidget(search,    0, 1);
    gl->addWidget(searchBtn, 0, 2);
    gl->addWidget(mediaList, 1, 0, 1, 2);
    gl->addWidget(vscr,      1, 2, 2, 1);

    searchBtn->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
    searchBtn->setCheckable(true);
    searchBtn->setShortcut(QKeySequence(Qt::CTRL + Qt::Key_F));

    search->setContentsMargins(5,0,0,0);
    search->setMinimumWidth(300);
    search->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);
    search->setVisible(false);

    mediaList->setEditTriggers(QAbstractItemView::DoubleClicked);
    mediaList->setFrameStyle(QFrame::NoFrame);
    mediaList->setAlternatingRowColors(true);
    mediaList->setSelectionMode(QAbstractItemView::ContiguousSelection);
    mediaList->setDragEnabled(true);
    mediaList->setAcceptDrops(true);
    mediaList->setDropIndicatorShown(true);
    mediaList->setDragDropOverwriteMode(false);
    mediaList->setDragDropMode(QAbstractItemView::InternalMove);    
    mediaList->setMovement(QListView::Free);
    mediaList->setVerticalScrollBarPolicy  (Qt::ScrollBarAlwaysOff);
    mediaList->setHorizontalScrollBarPolicy(Qt::ScrollBarAsNeeded);
    mediaList->setVerticalScrollBar(vscr);

    mlHeader->setStretchLastSection(false);
    mlHeader->setSectionsClickable(true);
    mlHeader->setSectionsMovable(true);
    mlHeader->setHighlightSections(true);
    mlHeader->setMaximumHeight(qMax(mlHeader->font().pixelSize() * 3.3,
                                    mlHeader->font().pointSize() * 3.3));

    QFrame *fr = new QFrame();
    fr->setFrameStyle(QFrame::Panel);
    fr->setLayout(gl);

    QVBoxLayout *vbl = new QVBoxLayout();
    vbl->setSpacing(0);
    vbl->addWidget(fr);

    filesPage->setLayout(vbl);

    // settings page ===================================================
    QWidget *settingsPage = new QWidget();

    QFormLayout *fl = new QFormLayout();
    fl->setLabelAlignment(Qt::AlignRight | Qt::AlignVCenter);
    fl->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint);
    fl->setSpacing(3);

    // Spinedit for server port ------------
    portSpin = new QSpinBox();
    portSpin->setAccelerated(true);
    portSpin->setKeyboardTracking(false);
    portSpin->setMinimum(c_port_min);
    portSpin->setMaximum(c_port_max);
    portSpin->setMaximumWidth(portSpin->fontMetrics().width("99999") + 30);
    portSpin->setValue(s.value("server_port", c_default_network_port).toInt());
    portSpin->setToolTip(tr("Port at which VocaSeed will listen to requests.")
                         + QString(" [%1..%2]").arg(c_port_min).arg(c_port_max));

    fl->addRow(tr("Port") + ":", portSpin);

    // Checkbox for hiding to tray ---------
    hideToTray = s.value("hide_to_tray", /*trayAvailable*/false).toBool();  // asked to disable by default

    QCheckBox *hideToTrayCHB = new QCheckBox();
    hideToTrayCHB->setChecked(hideToTray);
    hideToTrayCHB->setEnabled(trayAvailable);
    hideToTrayCHB->setToolTip(tr("Hide VocaSeed to tray (as a tray icon) instead of minimizing."));

    connect(hideToTrayCHB, &QCheckBox::stateChanged, [this](int state) { hideToTray = state != 0; });

    fl->addRow(tr("Hide to tray") + ":", hideToTrayCHB);

    // Checkbox for showing number of new logs in log tab header ----------
    logNotify = s.value("new_logs_notification", true).toBool();

    QCheckBox *logNotifyCHB = new QCheckBox();
    logNotifyCHB->setChecked(logNotify);
    logNotifyCHB->setToolTip(tr("Show number of new logs in log tab header if any other tab is active."));

    connect(logNotifyCHB, &QCheckBox::stateChanged, [this](int state) { logNotify = state != 0; });

    fl->addRow(tr("Notify on new logs") + ":", logNotifyCHB);

    QCheckBox *copyHTMLCHB = new QCheckBox();
    copyHTML = s.value("copy_html", false).toBool();
    copyHTMLCHB->setChecked(copyHTML);
    copyHTMLCHB->setToolTip(tr("Append .html to copied ID to serve MediaJS player instead of raw media file."));

    connect(copyHTMLCHB, &QCheckBox::stateChanged, [this](int state) { copyHTML = state != 0; });

    fl->addRow(tr("Copy ID with HTML") + ":", copyHTMLCHB);

    // Edit for url to access this machine from internet, used for copying ID ---------
    url = new QLineEdit(s.value("my_url", "http://localhost:39039/").toString());
    url->setToolTip(tr("URL to access this machine from Internet, used for copying IDs and serving HTML."));
    url->setMinimumWidth(url->fontMetrics().width(url->text()) + 20);
    url->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);

    connect(url, &QLineEdit::textChanged, [this](const QString &txt)
    {
        int w = url->fontMetrics().width(txt) + 20;
        url->setMinimumWidth(w);
    });

    fl->addRow(tr("My URL") + ":", url);

    // Edit for url to access this machine from internet, used for copying ID ---------
    ipCheckServiceStr = s.value("ip_check_service", "http://www.geoiptool.com/?IP=%1").toString();
    setIPService(ipCheckServiceStr);

    ipCheckService = new QLineEdit(ipCheckServiceStr);
    ipCheckService->setToolTip(tr("Internet service to check IPs in log, '%1' is replaced with IP."));
    ipCheckService->setMinimumWidth(ipCheckService->fontMetrics().width(ipCheckService->text()) + 20);
    ipCheckService->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Preferred);

    connect(ipCheckService, &QLineEdit::textChanged, [this](const QString &txt)
    {
        int w = ipCheckService->fontMetrics().width(txt) + 20;
        ipCheckService->setMinimumWidth(w);\
    });
    connect(ipCheckService, &QLineEdit::editingFinished, [this]()
    {
        ipCheckServiceStr = ipCheckService->text();
        setIPService(ipCheckServiceStr);
    });

    fl->addRow(tr("IP check service") + ":", ipCheckService);

    // Spinedit for folder scan frequency ------------
    fsIntervalSpin = new QSpinBox();
    fsIntervalSpin->setAccelerated(true);
    fsIntervalSpin->setKeyboardTracking(false);
    fsIntervalSpin->setMinimum(1);
    fsIntervalSpin->setMaximum(60);
    fsIntervalSpin->setMaximumWidth(fsIntervalSpin->fontMetrics().width("99") + 30);
    fsIntervalSpin->setValue(s.value("folder_scan_interval", 10).toInt());
    fsIntervalSpin->setToolTip(tr("Time in seconds between scanning watched folders for new files."));

    fl->addRow(tr("Folder scan interval") + ":", fsIntervalSpin);

    QTextBrowser *setDesc = new QTextBrowser(this);
    setDesc->setReadOnly(true);
    setDesc->setOpenExternalLinks(true);
    setDesc->setContextMenuPolicy(Qt::NoContextMenu);
    setDesc->setAcceptRichText(true);
    setDesc->setMinimumHeight(100);
    setDesc->setWordWrapMode(QTextOption::WordWrap);

    setDesc->setText(QString("%1 @ %2").arg(c_vs_version).arg(__DATE__));
    setDesc->append("");
    setDesc->append(tr("Media webseed client to stream audio/video into browsers (HTML5), requires real IP."));
    setDesc->append(tr("FOSS from <a href=\"http://code.google.com/p/vocatube-webseed-client/\">Google Code</a>, BSD license."));
    setDesc->append("");
    setDesc->append(tr("Add files with drag'n'drop, set your url and port (as they're accessible from internet), ") +
                    tr("copy files' URLs with rmb/ctrl+c, open in web browser, enjoy."));
    setDesc->append("");
    setDesc->append(tr("CTRL+F - filter list"));
    setDesc->append(tr("CTRL+C - copy URL(s)"));
    setDesc->append(tr("Del - remove from list"));
    setDesc->append(tr("Backspace - change ID"));

    QPushButton *wfAdd = new QPushButton(QIcon(":/icons/res/add.png"), "");
    connect(wfAdd, &QPushButton::clicked, [this]()
    {
        const QString &newWatchedDir = QFileDialog::getExistingDirectory(this, tr("Select a new watched directory"), lastDir);

        if (!(newWatchedDir.isEmpty() || watchedFolders.contains(newWatchedDir)))
        {
            lastDir = QFileInfo(newWatchedDir).absoluteDir().absolutePath();
            emit watchFolder(newWatchedDir);
        }
    });

    watchedFList = new QVBoxLayout(this);
    watchedFList->setContentsMargins(3,2,3,2);
    watchedFList->setSpacing(2);
    watchedFList->addWidget(wfAdd);

    QGroupBox *setWFGroup = new QGroupBox(tr("Watched folders"), this);
    setWFGroup->setLayout(watchedFList);

    QHBoxLayout *flHBL = new QHBoxLayout();
    flHBL->setContentsMargins(0,0,0,0);
    flHBL->setSpacing(0);
    flHBL->addLayout(fl);
    flHBL->addStretch(9001);

    QHBoxLayout *wfHBL = new QHBoxLayout();
    wfHBL->setContentsMargins(0,0,0,0);
    wfHBL->setSpacing(0);
    wfHBL->addWidget(setWFGroup);
    wfHBL->addStretch(9001);

    QVBoxLayout *settingsRowsVBL = new QVBoxLayout();
    settingsRowsVBL->setSpacing(5);
    settingsRowsVBL->addLayout(flHBL);
    settingsRowsVBL->addLayout(wfHBL);
    settingsRowsVBL->addStretch(9001);

    QScrollArea *setScrl = new QScrollArea(this);
    QWidget *setScrlW = new QWidget(setScrl);
    setScrlW->setPalette(setDesc->palette());
    setScrlW->setLayout(settingsRowsVBL);
    setScrl->setWidget(setScrlW);
    setScrl->setWidgetResizable(true);

    QHBoxLayout *setHBL = new QHBoxLayout();
    setHBL->setSpacing(2);
    setHBL->addWidget(setScrl);
    setHBL->addWidget(setDesc);

    settingsPage->setLayout(setHBL);

    // log page ========================================================
    QWidget *logPage = new QWidget();
    logPage->setLayout(new QVBoxLayout());

    logWidget = new QTextBrowser(this);
    logWidget->setReadOnly(true);
    logWidget->setOpenExternalLinks(true);
    logWidget->setContextMenuPolicy(Qt::NoContextMenu);
    logWidget->setAcceptRichText(true);

    logPage->layout()->addWidget(logWidget);

    // combining stuff together ========================================
    tabs = new QTabWidget();

    tabs->addTab(filesPage,     QIcon(), c_files_tab_name);
    tabs->addTab(settingsPage,  QIcon(), c_settings_tab_name);
    tabs->addTab(logPage,       QIcon(), c_log_tab_name);

    connect(tabs, &QTabWidget::currentChanged, [this](int tabI)
    {
        if (tabI == c_log_tab_index)
        {
            tabs->setTabText(c_log_tab_index, c_log_tab_name);
            unreadLogMsgs = 0;
        }
    });

    setLayout(new QVBoxLayout(this));
    this->layout()->setMargin(0);
    this->layout()->addWidget(tabs);

    setWindowIcon(QIcon(":/icons/res/appicon.png"));
    show();
}

vsWindow::~vsWindow()
{
    //
}

void vsWindow::onLogMessage(QString msg, vsLog::msgType type)
{
    if (type == vsLog::error)        msg = QString("<pre style=\"color:red;\">")   + msg + QString("</pre>");
    else if (type == vsLog::success) msg = QString("<pre style=\"color:green;\">") + msg + QString("</pre>");
    else                             msg = QString("<pre>")                        + msg + QString("</pre>");

    msg += QString("<p></p>");

    logWidget->moveCursor(QTextCursor::End);
    logWidget->insertHtml(msg);
    logWidget->verticalScrollBar()->setValue(logWidget->verticalScrollBar()->maximum());

    if (logNotify)
        if (tabs->currentIndex() != c_log_tab_index)
            tabs->setTabText(c_log_tab_index, QString("%1 [%2]").arg(c_log_tab_name).arg(++unreadLogMsgs));
}

void vsWindow::changeEvent(QEvent *e)
{
    switch (e->type())
    {
    case QEvent::WindowStateChange:
        if (this->windowState() & Qt::WindowMinimized)
        {
            if (hideToTray)
            {
                trayIcon->setVisible(true);
                QTimer::singleShot(0, this, SLOT(hide()));
            }
        }
        else trayIcon->setVisible(false);
        break;
    default:
        break;
    }

    QWidget::changeEvent(e);
}

void vsWindow::closeEvent(QCloseEvent *e)
{
    e->ignore();
    onExit();
}

void vsWindow::iconActivated(QSystemTrayIcon::ActivationReason reason)
{
    switch (reason)
    {
    case QSystemTrayIcon::Trigger:
    case QSystemTrayIcon::DoubleClick:
    case QSystemTrayIcon::MiddleClick:
        showNormal();
        raise();
        setFocus();
        break;
    default: break;
    }
}

void vsWindow::addWatchedFolder(const QString &path)
{
    if (!(path.isEmpty() || watchedFolders.contains(path)))
    {
        QWidget *wPanel = new QWidget(this);

        QLabel *wfLbl = new QLabel(path, wPanel);
        QPushButton *wfDel = new QPushButton(QIcon(":/icons/res/delete.png"), "");
        connect(wfDel, &QPushButton::clicked, [this, path]() { emit stopWatchingFolder(path); });

        QHBoxLayout *hbl = new QHBoxLayout();
        hbl->addWidget(wfDel);
        hbl->addWidget(wfLbl);
        hbl->addStretch(9001);
        hbl->setContentsMargins(0,0,0,0);

        wPanel->setLayout(hbl);
        watchedFolders[path] = wPanel;

        assert(watchedFList->count() > 0); // should never happen, must have at least an add button
        watchedFList->insertWidget(watchedFList->count() - 1, wPanel);
    }
}

void vsWindow::removeWatchedFolder(const QString &path)
{
    if (!path.isEmpty() && watchedFolders.contains(path))
    {
        QWidget *wPanel = watchedFolders[path];

        if (wPanel)
        {
            watchedFList->removeWidget(wPanel);
            delete wPanel;
            wPanel = 0;
            watchedFolders.remove(path);
        }
    }
}

void vsWindow::onExit()
{
    QSettings s(c_def_appsettings_init);

    if (trayAvailable)
        s.setValue("hide_to_tray", hideToTray);

    if (!lastDir.isEmpty())
        s.setValue("last_directory", lastDir);

    s.setValue("my_url", url->text());
    s.setValue("new_logs_notification", logNotify);
    s.setValue("copy_html", copyHTML);
    s.setValue("ip_check_service", ipCheckServiceStr);

    trayIcon->setVisible(false);
    emit closeRequest();
}

//---------------------------------------------------

vsQtGui::vsQtGui(QObject *parent) : vsUI(parent) {}
vsQtGui::~vsQtGui()                              {}


void vsQtGui::onFileAdded(vsMediaFile *file)
{
    if (file != 0 && !files->hasMedia(file->id))
    {
        connect(file, SIGNAL(mediaInfoUpdated()), this, SLOT(onFileInfoUpdated()));

        QString statS = statusString(file->status);
        QString sizeS = (file->size > 0)     ? sizeToString(file->size)         : "";
        QString durS  = (file->duration > 0) ? durationToString(file->duration) : "";
        QString artS  = (file->tags.contains("artist")) ? file->tags["artist"]  : "";
        QString nameS = (file->tags.contains("title"))  ? file->tags["title"]   : file->name;

        if (nameS.isEmpty())
            nameS = file->name;

        files->addMedia(vsMLRow(file->id, nameS, artS, file->path, statS, sizeS,file->mime, durS,
                                file->size, file->duration));
    }
}

void vsQtGui::onConnected()
{
    if (!initialized)
    {
        win = new vsWindow();

        // read settings and determine the headers (which will be used for any incoming data
        QSettings s(c_def_appsettings_init);

        // create and setup the app window
        win->setWindowTitle("VocaSeed");
        win->setWindowIcon(QIcon(":/icons/res/appicon.png"));
        win->move(s.value("window_pos_x", 100).toInt(), s.value("window_pos_y", 100).toInt());
        win->resize(s.value("window_width", c_window_width).toInt(),
                    s.value("window_height", c_window_height).toInt());

        files = new vsMLModel();
        files->setRowHeight(qMax(win->mediaList->font().pointSize() * 2.5,
                                 win->mediaList->font().pixelSize() * 2.5));
        QStringList mlhCols   = s.value("header_columns",       c_default_headers)      .toString().split(' ');
        QStringList mlhWidths = s.value("header_column_widths", c_default_header_widths).toString().split(' ');
        QList<int>  mlhWI;

        foreach (const QString &wS, mlhWidths)
            mlhWI.append(wS.toInt());

        foreach (const QString &hcID, mlhCols)
        {
            mlData::type t = getMLTypeForTypeString(hcID);

            if (t >= mlData::id && t < mlData::END)
                headerCols[t] = true;
        }

        setupMLHeader(mlhCols, mlhWI);

        win->mlDrawer = new vsMLDelegate(win->mediaList, files);
        win->mediaList->setModel(files);
        win->mediaList->installEventFilter(this);

        // set correct items width using header total width
        files->onMLHeaderResized(0, win->mlHeader->sectionSize(0), win->mlHeader->length());

        win->mediaList->setContextMenuPolicy(Qt::CustomContextMenu);
        connect(win->mediaList, &QListView::customContextMenuRequested, this, &vsQtGui::onMLMenu);
/*
        void sectionMoved(int logicalIndex, int oldVisualIndex, int newVisualIndex);
        void sectionResized(int logicalIndex, int oldSize, int newSize);
        void sectionPressed(int logicalIndex);
        void sectionClicked(int logicalIndex);
        void sectionEntered(int logicalIndex);
        void sectionDoubleClicked(int logicalIndex);
        void sectionCountChanged(int oldCount, int newCount);
        void sectionHandleDoubleClicked(int logicalIndex);
        void geometriesChanged();
        void sortIndicatorChanged(int logicalIndex, Qt::SortOrder order);
*/
        win->mlHeader->setContextMenuPolicy(Qt::CustomContextMenu);
        connect(win->mlHeader,  &QHeaderView::customContextMenuRequested, this, &vsQtGui::onMLHeaderMenu);
        connect(win->mlHeader,  &QHeaderView::sectionClicked, [this](int section)
            { files->sort(section, sortOrderForHeaderSection(section)); });

        connect(win->mlHeader,  &QHeaderView::sectionResized, [this](int logicalInd, int /*oldS*/, int newS)
            { files->onMLHeaderResized(win->mlHeader->visualIndex(logicalInd), newS, win->mlHeader->length()); });

        connect(win->mlHeader,  &QHeaderView::sectionMoved,   [this](int /*logicalInd*/, int oldVI, int newVI)
            { files->onMLHeaderReordered(oldVI, newVI); });

        connect(win->mediaList->horizontalScrollBar(), &QScrollBar::valueChanged, win->mlHeader, &QHeaderView::setOffset);
        connect(win->mlDrawer, &vsMLDelegate::idSet, this, &vsQtGui::mediaIDChanged);

        connect(win->portSpin,       SIGNAL(valueChanged(int)), this, SLOT(onPortSet(int)));
        connect(win->fsIntervalSpin, SIGNAL(valueChanged(int)), this, SLOT(onFWScanIntervalSet(int)));

        connect(win->filesPage, &dWgt::fileDropped, [&](const QString &path){ emit addFile(path, "");           });
        connect(win->url,       &QLineEdit::editingFinished, [&]()          { emit urlEdited(win->url->text()); });

        connect(win->searchBtn, &QPushButton::toggled,  this,  &vsQtGui::onSearchToggled);
        connect(win->search,    &QLineEdit::textEdited, files, &vsMLModel::filter);

        connect(win, &vsWindow::watchFolder,        this, &vsQtGui::watchedFolderAdded  );
        connect(win, &vsWindow::stopWatchingFolder, this, &vsQtGui::watchedFolderRemoved);

        connect(win, SIGNAL(closeRequest()), this, SLOT(onExit()));

        // logging ---------------------------------------------------
        vsLog* log = vsLog::instance();
        connect(log, &vsLog::message, win, &vsWindow::onLogMessage);

        log->lastTime.clear();  // to print time of greet msg for sure
        vsLog::i(c_greet_msg + " @ " + __DATE__);
        vsLog::i("------------- Initialization log: ----------------");

        log->history.removeLast();
        log->history.removeLast();

        foreach (QString s, log->history)
            vsLog::r(s);

        vsLog::i("--------------------------------------------------");

        initialized = true;
    }
}

void vsQtGui::onSearchToggled(bool on)
{
    if (on)
    {
        win->search->setVisible(true);
        win->search->setFocus();
    }
    else
    {
        win->search->clear();
        win->search->setVisible(false);
        files->filter("");
    }
}

void vsQtGui::setupMLHeader(const QStringList &ids, const QList<int> &widths)
{
    files->clearHeaderData();
    files->rearrangeHeaders(ids);
    files->resizeHeaders(widths);

    vsMLModel tmp; // because QHeaderView->reset() doesn't reset logical indexes, sorting headers then messes titles
    win->mlHeader->setModel(&tmp);
    win->mlHeader->setModel(files);
    int hI = 0;

    // sadly, QHeaderView asks model for everything except SizeHint, so have to resize sections manually
    foreach (const int &w, widths)
    {
        win->mlHeader->resizeSection(hI, w);
        ++hI;
    }
}

QStringList vsQtGui::getIdOrder() const
{
    return files->mediaIDList();
}

bool vsQtGui::eventFilter(QObject *obj, QEvent *e)
{
    bool handled = false;

    if (obj == win->mediaList && e->type() == QEvent::KeyPress)
    {
        QKeyEvent *ke = static_cast<QKeyEvent*>(e);

        if (ke->key() == Qt::Key_Delete)
        {
            onDeleteSelectedMedias();
            handled = true;
        }
        else if (ke->key() == Qt::Key_C && ke->modifiers().testFlag(Qt::ControlModifier))
        {   // copy urls
            onCopySelectedURLs();
            handled = true;
        }
        else if (ke->key() == Qt::Key_Backspace)
        {
            const QModelIndexList &selIds = win->mediaList->selectionModel()->selectedIndexes();

            if (!selIds.isEmpty())
            {
                win->mediaList->edit(selIds.first());
                handled = true;
            }
        }
    }

    return handled ? true : QObject::eventFilter(obj, e);
}

void vsQtGui::onMLHeaderMenu(const QPoint &pos)
{
    if (!mlHeaderMenu)
    {
        mlHeaderMenu = new QMenu(win);

        for (int t = mlData::id; t < mlData::END; ++t)
        {
            QAction *a = new QAction(getStringForMLType((mlData::type)t), mlHeaderMenu);
            a->setCheckable(true);
            a->setChecked(headerCols[t]);
            a->setData(t);

            connect(a, &QAction::toggled, this, &vsQtGui::onHeaderColToggled);
            mlHeaderMenu->addAction(a);
        }
    }

    mlHeaderMenu->popup(win->mlHeader->mapToGlobal(pos));
}

void vsQtGui::onMLMenu(const QPoint &pos)
{
    if (!mlMenu)
    {
        mlMenu = new QMenu(win->mediaList);

        copyURLAction   = mlMenu->addAction(QIcon(":/icons/res/copy.png"),
                                          tr("Copy URL"), this, SLOT(onCopySelectedURLs()));
        showFilesAction = mlMenu->addAction(QIcon(":/icons/res/explore.png"),
                                              tr("Show"), this, SLOT(onShowSelectedFiles()));
        mlMenu->addSeparator();
        addMediasAction = mlMenu->addAction(QIcon(":/icons/res/add.png"),
                                          tr("Add more"), this, SLOT(onMLMenuAddFiles()));
        mlMenu->addSeparator();
        removeMediasAction = mlMenu->addAction(QIcon(":/icons/res/delete.png"),
                                  tr("Remove from list"), this, SLOT(onDeleteSelectedMedias()));
    }

    bool selection = !win->mediaList->selectionModel()->selectedIndexes().isEmpty();

    copyURLAction     ->setEnabled(selection);
    showFilesAction   ->setEnabled(selection);
    removeMediasAction->setEnabled(selection);

    mlMenu->popup(win->mediaList->mapToGlobal(pos));
}

void vsQtGui::onMLMenuAddFiles()
{
    // setting up extension filter ----------------------
    QString filter;
    QMap<QString, QString> supportedFileTypes = vsMedia::instance()->supportedTypes();

    foreach (const QString &ext, supportedFileTypes.keys())
        filter.append(QString("*.%1 ").arg(ext));

    if (!filter.isEmpty())
        filter.chop(1); // remove last space

    filter = QString("Media files (%1)").arg(filter);
    //---------------------------------------------------

    QStringList fileNames = QFileDialog::getOpenFileNames(win, tr("Select media files"), win->lastDir, filter);

    foreach (const QString &file, fileNames)
        emit addFile(file, "");
}

void vsQtGui::onHeaderColToggled(bool on)
{
    QAction *a = dynamic_cast<QAction*>(sender());

    if (a)
    {
        mlData::type t = (mlData::type)a->data().toInt();

        if (t >= mlData::id && t < mlData::END)
        {
            QStringList headerColIDs = files->headerTypeStrings();
            QList<int>  headerColWs  = files->headerSizes();
            QString id = getTypeStringForMLType(t);

            if (!id.isEmpty())
            {
                bool hadID = headerColIDs.contains(id);

                if (on && !hadID) // add new column to header
                {
                    headerColIDs.append(id);
                    headerColWs .append(100); // default column width

                    setupMLHeader(headerColIDs, headerColWs);
                }
                else if (!on && hadID) // remove an existing column from header
                {
                    int i = headerColIDs.indexOf(id);
                    Q_ASSERT(i > -1); // should never happen

                    headerColIDs.removeAt(i);
                    headerColWs .removeAt(i);

                    setupMLHeader(headerColIDs, headerColWs);
                }
            }
        }
    }
}

void vsQtGui::onFileInfoUpdated()
{
    vsMediaFile *f = dynamic_cast<vsMediaFile*>(sender());

    if (f && files->hasMedia(f->id))
    {
        QMap<mlData::type, QVariant> info;

        info[mlData::duration] = f->duration;
        info[mlData::status]   = statusString(f->status);

        QString titleStr = f->tags.value("title");
        QString artistStr = f->tags.value("artist");

        if (!titleStr .isEmpty()) info[mlData::name]   = titleStr;
        if (!artistStr.isEmpty()) info[mlData::artist] = artistStr;

        files->updateMedia(f->id, info);
    }
}

void vsQtGui::onNetworkInfoUpdated(QString id, int requests, int uprate, int dlrate, qint64 seeded)
{
    if (!id.isEmpty() && files->hasMedia(id))
    {
        QMap<mlData::type, QVariant> info;

        info[mlData::uprate] = (uprate   > 0) ? sizeToString(uprate) + "/" + QObject::tr("s") : "";
        info[mlData::dlrate] = (dlrate   > 0) ? sizeToString(dlrate) + "/" + QObject::tr("s") : "";

        if (requests > 0) info[mlData::requests] = requests;
        if (seeded   > 0) info[mlData::seeded]   = seeded;

        files->updateMedia(id, info);
    }
}


void vsQtGui::onExit()
{
    QSettings s(c_def_appsettings_init);
    s.setValue("window_pos_x",  win->pos().x());
    s.setValue("window_pos_y",  win->pos().y());
    s.setValue("window_width",  win->width() );
    s.setValue("window_height", win->height());

    // save header settings
    QString headerStr;
    QString headerColumnWidths;

    foreach (const QString &hID, files->headerTypeStrings())
        headerStr.append(hID).append(" ");

    foreach (const int &hW, files->headerSizes())
        headerColumnWidths.append(QString("%1 ").arg(hW));

    if (!headerStr.isEmpty())          headerStr.chop(1); // remove last space
    if (!headerColumnWidths.isEmpty()) headerColumnWidths.chop(1);

    s.setValue("header_columns",       headerStr);
    s.setValue("header_column_widths", headerColumnWidths);

    emit exiting();
}

void vsQtGui::onPortSet(int newPort)
{
    if (newPort >= 20 && newPort <= 64000)
        emit portChanged(newPort);
    else
        win->portSpin->setValue(39390);
}

void vsQtGui::onFWScanIntervalSet(int newInterval)
{
    if (newInterval >= 1 && newInterval <= 60)
        emit fwScanIntervalChanged(newInterval);
    else
        win->fsIntervalSpin->setValue(10);
}


// received from logic
void vsQtGui::onFileRemoved(QString id)
{   // remove id from ui completely, no callbacks
    files->removeMedia(id);
}

void vsQtGui::onCopySelectedURLs()
{
    const QModelIndexList selected = win->mediaList->selectionModel()->selectedIndexes();

    if (!selected.isEmpty())
    {
        QList<int> selectedRows;

        foreach (const QModelIndex &si, selected)
            selectedRows.append(si.row());

        QStringList selIDs = files->valuesOfRows(selectedRows, mlData::id);
        QString copiedIDs;

        foreach (const QString &id, selIDs)
            copiedIDs.append(QString("%1%2.vs%3\r\n").arg(win->url->text()).arg(id).arg((win->copyHTML) ? ".html" : ""));

        copiedIDs.chop(2);
        QApplication::clipboard()->setText(copiedIDs);
#ifdef Q_OS_LINUX
        QApplication::clipboard()->setText(copiedIDs, QClipboard::Selection); // set X selection
#endif
    }
}

void vsQtGui::onShowSelectedFiles()
{
    const QModelIndexList selected = win->mediaList->selectionModel()->selectedIndexes();

    if (!selected.isEmpty())
    {
        QMap<QString, bool> openedFolders;
        QList<int> selectedRows;

        foreach (const QModelIndex &si, selected)
            selectedRows.append(si.row());

        QStringList paths = files->valuesOfRows(selectedRows, mlData::path);

        foreach (const QString &p, paths)
        {
            QString dir = QFileInfo(p).absolutePath();

            if (!openedFolders.contains(dir))
            {
                openedFolders[dir] = true;
#if defined(Q_OS_WIN) //-------------------------------
                QStringList args;
                args << "/select," << QDir::toNativeSeparators(p);
                QProcess::startDetached("explorer", args);
#elif defined (Q_OS_MAC) //----------------------------
                QStringList args;
                args << "-e";
                args << "tell application \"Finder\"";
                args << "-e";
                args << "activate";
                args << "-e";
                args << "select POSIX file \""+p+"\"";
                args << "-e";
                args << "end tell";
                QProcess::startDetached("osascript", args);
#else //-----------------------------------------------
                QDesktopServices::openUrl(QUrl::fromLocalFile(p));
#endif
            }

            if (openedFolders.size() >= 20)
                break; // no more, or it'll DoS OS
        }
    }

}

void vsQtGui::onDeleteSelectedMedias()
{
    const QModelIndexList selected = win->mediaList->selectionModel()->selectedIndexes();

    if (!selected.isEmpty())
    {
        QList<int> selectedRows;

        foreach (const QModelIndex &si, selected)
            selectedRows.append(si.row());

        QStringList selIDs = files->valuesOfRows(selectedRows, mlData::id);

        foreach (const QString &id, selIDs)
            emit removeMedia(id);
    }
}

void vsQtGui::onMediaIDChanged(QString oldID, vsMediaFile *updatedData)
{   // received from logic. Change id in ui, no callbacks
    assert(updatedData != 0);
    assert(files->changeMediaID(oldID, updatedData->id)); // assuming that nothing else changed
}

void vsQtGui::watchFolder       (const QString &path) { win->addWatchedFolder(path);   }
void vsQtGui::stopWatchingFolder(const QString &path) { win->removeWatchedFolder(path);}
